import subprocess
from subprocess import PIPE
import os
import re
import sys
import shutil
import errno
import time
import datetime
import logging
import logging.config
import MySQLdb as sql

#--- LOGGING FACILITY ---
logging.config.fileConfig('/home/robert/backup_log.txt')
logProcManager = logging.getLogger('proc')
logSQL = logging.getLogger('sql')
logTar = logging.getLogger('tar')
logRsync = logging.getLogger('rsync')
logMain = logging.getLogger('main')
#--- END LOGGING FACILITY ---

#--- MYSQL INFO ---
MYSQL_USER = 'root'
MYSQL_PASS = ''
MYSQL_DB = 'newznab'
#--- END MYSQL INFO ---

if not MYSQL_PASS:
    logMain.error('No MYSQL password entered.  Please add a password for MYSQL_PASS at approximately line 25.')
    sys.exit()


class BackupManager(object):

    """
    backupPath:     destination for completed backups
    tempPath:       temporary location for backups before copying to backupPath
    trimInterval:   backups to keep
    dateFormat:     string for date to use as directory
    dateFileFormat: date/filetype tag for naming backup files
    backupJobs:     list of tuples in format (function, **kwargs)
    tempFiles:      list of full paths for files to be copied to backupPath

    getFormattedFile() : create a file path in the format tempPath/name-date-time.tar.gz
    """

    def __init__(self, backupPath, intervalCount):
        self.date = datetime.datetime.now()
        self.dateISO = self.date.isocalendar()
        self.dateFormat = '{}-{}-{}'.format(self.date.year, str(self.date.month).zfill(2),
                                            str(self.date.day).zfill(2))
        self.dateFileFormat = '-{}T{}-{}.tar.gz'.format(self.dateFormat, str(self.date.hour).zfill(2),
                                                        str(self.date.minute).zfill(2))
        self.backupRootPath = backupPath
        self.backupPath = os.path.join(backupPath, self.dateFormat)
        self.trimInterval = intervalCount
        self.datePattern = re.compile(r'^\d\d\d\d-\d\d-\d\d$')
        self.backupJobs = []
        self.tempFiles = []

    def makeTempPath(self, path):
        if os.path.exists(path):
            self.tempPath = path + self.dateFormat
            os.makedirs(self.tempPath)
            return False
        else:
            self.tempPath = path
            os.makedirs(self.tempPath)
            return True

    def makeBackupPath(self):
        if not os.path.exists(self.backupPath):
            os.makedirs(self.backupPath)

    def rmTempPath(self):
        shutil.rmtree(self.tempPath)

    def getInterFile(self, name):
        return os.path.join(self.tempPath, name)

    def getTempBackupFiles(self):
        self.tempFiles =  [os.path.join(self.tempPath, e) for e in os.listdir(self.tempPath)
                           if self.dateFileFormat in e]
        return self.tempFiles

    def copyToBackupPath(self):
        for fname in self.getTempBackupFiles():
            shutil.copy2(fname, self.backupPath)

    def getBackupsToTrim(self):
        if not self.trimInterval:
            return []
        return [os.path.join(self.backupRootPath, e) for e in os.listdir(self.backupRootPath)
                if re.match(self.datePattern, e)
                and (self.date - datetime.datetime.strptime(e, '%Y-%m-%d')).days > self.trimInterval]

    def trimBackups(self, oldBackups):
        for old in oldBackups:
            shutil.rmtree(old)

    def getFormattedFile(self, name):
        return os.path.join(self.tempPath, (name + self.dateFileFormat))

    def addBackupJob(self, func, **kwargs):
        self.backupJobs.append((func, kwargs))

    def callBackupJob(self, job):
        job[0](**job[1])


class ProcessManager(object):

    def getPID(self, procName):
        pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
        foundPID = []
        for pid in pids:
            try:
                with open(os.path.join('/proc', pid, 'cmdline'), 'r') as f:
                    process = f.read()
                if procName in process:
                    foundPID.append(int(pid))
            except:
                logProcManager.warning('Error opening PID {}'.format(pid))
        return None

    def waitForProcPause(self, procName, sigPause, sigConfirm):
        t_wait = 0
        confirmSafe = None
        if self.getPID(procName):
            logProcManager.info('Process {} running.'.format(procName))
            if not sigPause in self.readRecallFile():
                self.writeRecallFile(sigPause)
            while not confirmSafe and t_wait < 3600:
                logProcManager.info('Waiting for {} to pause'.format(procName))
                time.sleep(120)
                t_wait += 120
                if sigConfirm in self.readRecallFile():
                    logProcManager.info('Process paused.')
                    return True
            logProcManager.warning('Could not pause process.')
            confirmSafe = False
        else:
            logProcManager.warning('Process {} not found.'.format(procName))
            confirmSafe = True
        return confirmSafe

    def killProc(self, procName):
        pids = self.getPID(procName)
        if not pids:
            return False
        else:
            for pid in pids:
                try:
                    os.kill(pid, 2)
                except OSError as e:
                    if e.errno == errno.EPERM or e.errno == errno.ESRCH:
                        return False
                    raise
            else:
                return True


    def makeRecallFile(self, recallFile):
        self.recallFile = recallFile
        if not os.path.exists(self.recallFile):
            open(self.recallFile, 'w').close()
            os.chmod(self.recallFile, 0777)

    def readRecallFile(self):
        with open(self.recallFile, 'r') as f:
            return f.read()

    def writeRecallFile(self, data):
        with open(self.recallFile, 'a') as f:
            f.write(data)

    def rmRecallFile(self):
        os.remove(self.recallFile)


def dumpSQL(destination=None):
    DUMP_CMD = 'mysqldump --user={} --password={} {}'.format(MYSQL_USER, MYSQL_PASS, MYSQL_DB)
    logSQL.info('Beginning dump process.')
    dump = subprocess.Popen(DUMP_CMD, shell=True, stdout=PIPE, stderr=PIPE)
    logSQL.info('Dump completed; processing.')
    try:
        with open(destination, 'w') as f:
            logSQL.info('Writing file.')
            f.writelines(dump.stdout)
    except Exception as exc:
        logSQL.error('Error: {}'.format(exc))
        return False
    else:
        logSQL.info('File written successfully.')
        return True


def tarFiles(source=None, destination=None, exclude=[]):
    logTar.info('Source {}'.format(source))
    logTar.info('Destination {}'.format(destination))
    logTar.info('Exclude: {}'.format(str(exclude)))
    cmd = ['tar']
    params = ['-czf', destination]
    if exclude:
        params.extend(['--exclude={}'.format(e) for e in exclude])
    params.append(source)
    cmd.extend(params)
    try:
        subprocess.check_call(cmd)
    except subprocess.CalledProcessError as e:
        logTar.error('Error backing up: {}'.format(e))
    except Exception as e:
        logTar.error('Some other error occurred: {}'.format(e))
    else:
        logTar.info('Backup process successful.')

def doRsync(source=None, destination=None):
    logRsync.info('Source {}'.format(source))
    logRsync.info('Destination {}'.format(destination))
    cmd = ['rsync', '-avr', '--no-o', '--no-g', '--progress', '--delete',
           '--exclude', '"tmpunrar"', source, destination]
    try:
        subprocess.check_call(cmd)
    except subprocess.CalledProcessError as e:
        logRsync.error('rsync failed: {}'.format(e))
    else:
        logRsync.info('Completed rsync.')


def doPermissionTest(manager):
    fileSystemPerm = False
    dbPerm = False
    logSQL.info('Checking DB connection.')
    try:
        sql.connect(user=MYSQL_USER, passwd=MYSQL_PASS, db=MYSQL_DB).close()
    except:
        logSQL.error('Could not connect to database.')
    else:
        dbPerm = True
    try:
        open(os.path.join(manager.backupRootPath, 'test.txt'), 'w').close()
        os.remove(os.path.join(manager.backupRootPath, 'test.txt'))
    except IOError as e:
        if e.errno == errno.EPERM:
            logMain.error('No permissions to destination.')
        else:
            logMain.error('File permission test failed: {}.'.format(e))
    else:
        logMain.info('File permission test succeeded.')
        fileSystemPerm = True
    return fileSystemPerm, dbPerm


def createBackupJobs(manager, fsPerm, dbPerm):
    logMain.info('Creating backup job order.')
    if dbPerm:
        logMain.info('Adding job: mysql-dump')
        manager.addBackupJob(dumpSQL, destination=manager.getInterFile('mysql.sql'))
        logMain.info('Adding job: mysql-zip')
        manager.addBackupJob(tarFiles, source=manager.getInterFile('mysql.sql'),
                             destination=manager.getFormattedFile('mysql'))
    logMain.info('Adding job: nginx')
    manager.addBackupJob(tarFiles, source='/etc/nginx',
                         destination=manager.getFormattedFile('nginx'))
    logMain.info('Adding job: nnsite')
    manager.addBackupJob(tarFiles, source='/var/www/newznab',
                         destination=manager.getFormattedFile('newznab'),
                         exclude=['nzbfiles', 'sphinxdata'])
    if fsPerm:
        logMain.info('Adding job: nzbfiles')
        manager.addBackupJob(doRsync, source='/var/www/newznab/nzbfiles',
                             destination='/mnt/backup/rsync')
    logMain.info('All jobs added.')


def pauseRunningProcesses(recallFile):
    procManager = ProcessManager()
    procManager.makeRecallFile(recallFile)
    # Pause safebackfill.py
    logProcManager.info('Attempting to pause safebackfill.py.')
    if not procManager.waitForProcPause('safebackfill.py', 'stop_update\n', 'update_waiting'):
        logProcManager.warning('Could not pause safebackfill.py.')
        return False

    # Pause justpp.py
    logProcManager.info('Looking for justpostprocessing.php.')
    if not procManager.killProc('justpostprocessing.php'):
        logProcManager.info('Could not kill process.')
        if not procManager.waitForProcPause('justpp.py', 'stop_post\n', 'post_waiting'):
            logProcManager.warning('Python script not paused.')
            return False
    return True



def doCleanup(manager):
    logMain.info('Cleaning up temporary directories.')
    manager.rmTempPath()


def main():
    logMain.info('Starting backup.')
    logMain.info('Pausing processes.')
    manager = BackupManager('/mnt/backup/files', 6)
    if not manager.makeTempPath('/tmp/backup'):
        logMain.warning('Path already exists; creating a new path {}'.format(manager.tempPath))

    if not pauseRunningProcesses('/tmp/backup/recall.txt'):
        logProcManager.error('Error pausing processes; stopping backup.')
        doCleanup(manager)
        sys.exit()
    else:
        logProcManager.info('All processes paused successfully.')

    fsPerm, dbPerm = doPermissionTest(manager)


    manager.makeBackupPath()
    createBackupJobs(manager, fsPerm, dbPerm)

    for job in manager.backupJobs:
        manager.callBackupJob(job)

    logMain.info('Copying temporary files to backup location.')
    manager.copyToBackupPath()

    toTrim = manager.getBackupsToTrim()
    for f in toTrim:
        logMain.info('Found old backup to trim: {}'.format(f))
    logMain.info('Removing old backups.')
    manager.trimBackups(toTrim)

    doCleanup(manager)
    logMain.info('Copying log file to backup directory and exiting.')
    shutil.copy('/var/log/nntools/backup.log', manager.backupPath)


if __name__ == '__main__':
    main()
